"""
Copyright 2009 Texas A&M University
 
Licensed under the Apache License, Version 2.0 (the "License"); you may not use 
this file except in compliance with the License. You may obtain a copy of the 
License at

http://www.apache.org/licenses/LICENSE-2.0
  
Unless required by applicable law or agreed to in writing, software distributed 
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
CONDITIONS OF ANY KIND, either express or implied. See the License for the 
specific language governing permissions and limitations under the License. 
"""

from logging import getLogger

import zope.interface

from metalib.interfaces import IMetalibSession, IMetalibConnection
from metalib.group import MetalibGroup
from metalib.exceptions import *

try:
    from xml.etree import cElementTree as etree
except ImportError:
    from xml.etree import ElementTree as etree


__all__ = ['MetalibSession']

log = getLogger('metalib.session')

def _ConvertXmlToDictRecurse(node):
    nodedict = {}
    
    if len(node.items()) > 0:
        # if we have attributes, set them
        nodedict.update(dict(node.items()))
    
    for child in node:
        # recursively add the element's children
        newitem = _ConvertXmlToDictRecurse(child)
        if nodedict.has_key(child.tag):
            # found duplicate tag, force a list
            if type(nodedict[child.tag]) is type([]):
                # append to existing list
                nodedict[child.tag].append(newitem)
            else:
                # convert to list
                nodedict[child.tag] = [nodedict[child.tag], newitem]
        else:
            # only one, directly set the dictionary
            nodedict[child.tag] = newitem

    if node.text is None: 
        text = ''
    else: 
        text = node.text.strip()
    
    if len(nodedict) > 0:            
        # if we have a dictionary add the text as a dictionary value (if there is any)
        if len(text) > 0:
            nodedict['_text'] = text
    else:
        # if we don't have child nodes or attributes, just set the text
        nodedict = text
        
    return nodedict
            
def ConvertXmlToDict(root):
    """
    Converts an XML file or ElementTree Element to a dictionary
    """
    # If a string is passed in, try to open it as a file
    if type(root) == type(''):
        root = etree.parse(root).getroot()
#    elif not isinstance(root, ElementTree.Element):
#        raise TypeError, 'Expected ElementTree.Element or file path string'

    return {root.tag: _ConvertXmlToDictRecurse(root)}


class MetalibSession(object):
    zope.interface.implements(IMetalibSession)
    
    def __init__(self, connection, id):
        self._connection = IMetalibConnection(connection)
        self._id = id
    
    def post(self, operation, **kwargs):
        """ Post to the connection that this session was created from, add the
        required session information into the post data """
        kwargs.setdefault('session_id',self._id)
        return self._connection.post(operation, **kwargs)
    
    def find(self, command, base, wait=False, filter={}, application=None):
        """ Perform a find command on the xserver 
        Base should be a list of dictionary(ies).  Each dictionary contains one 
        item with key 'find_base_001' and the database id as the value. The 
        list contains one or more dictionaries (databases).  
        E.g. [{'find_base_001': 'TEX03112'}, {'find_base_001': 'TEX02016'}]
         
        """
        if (wait) and wait != 'N':
            waitflag = 'Y'
        else:
            waitflag = 'N'
        try:
            response = self.post('find_request', find_base=base
                                 , find_request_command=command, wait_flag=waitflag
                                 , find_filter = filter)
        except:
            raise

        group_id = response.find('find_response/group_number')
        if (group_id is not None and group_id.text):
            return MetalibGroup(session=self, id=group_id.text)
          
        elif (response.find('*/local_error') is not None):
            error_code = response.find('*/local_error/error_code').text
            error_msg = response.find('*/local_error/error_text').text
            raise MetalibServerException(error_code, error_msg)
          
        #TODO: Handle an invalid xml response (non-error)
        raise MetalibException(etree.tostring(response))

    
    def retrieveQuickSets(self, association):
        """ Retrieve the quick sets based on the given association
        
        Association should be a dictionary. There are four options for its 
        contents:
        
        1. {'requester_ip': requester_ip_address }
        2. {'source_id': source_id, 'institute': institute, 'verification': verification}
        3. {'bor_id': bor_id, 'verification': verification}
        4. {'pds_handle': pds_handle}
        
        Optional contents
        'filter_ird_authorization': 'Y' 
        'requester_ip_address': requester_ip_address
        """
        try:
            response = self.post('retrieve_quick_sets_request', **association)
        except:
            raise
        if (response.find('retrieve_quick_sets_response/set_info') is not None):
            qsets = ConvertXmlToDict(response)
            return qsets
          
        elif (response.find('*/local_error') is not None):
            error_code = response.find('*/local_error/error_code').text
            error_msg = response.find('*/local_error/error_text').text
            raise MetalibServerException(error_code, error_msg)
          
        #TODO: Handle an invalid xml response (non-error)
        raise MetalibException(etree.tostring(response))
          
    
    def searchQuickSet(self, set, command, wait=False, filter={},
                       application=None):
        """ Perform a search of a quick set 
        
        wait=false - asynchronic retrieval of the results
        wait=true  - synchonic retrieval of the results
        
        filter should be a dictionary. There are seven options for its 
        contents:
        
        1. {'requester_ip': requester_ip_address, 'institute': institute, 'user_group': user_group}
        2. {'requester_ip': requester_ip_address, 'institute': institute}
        3. {'requester_ip': requester_ip_address}
        4. {'institute': institute}
        5. {'institute': institute, 'user_group': user_group}
        6. {'requester_ip': requester_ip_address, 'pds_handle': pds_handle}
        7. {'pds_handle': pds_handle}
        """
        if (wait) and wait != 'N':
            waitflag = 'Y'
        else:
            waitflag = 'N'
        try:
            response = self.post('search_by_quick_set_request', quick_sets_id=set
                             , find_request_command=command, wait_flag=waitflag
                             , filter_ird_authorization = filter)
        except:
            raise
        
        group_id = response.find('find_response/group_number')
        if (group_id is not None and group_id.text):
            return MetalibGroup(session=self, id=group_id.text)
          
        elif (response.find('*/local_error') is not None):
            error_code = response.find('*/local_error/error_code').text
            error_msg = response.find('*/local_error/error_text').text
            raise MetalibServerException(error_code, error_msg)
          
        #TODO: Handle an invalid xml response (non-error)
        raise MetalibException(etree.tostring(response))
   
    def searchMySet(self, set, command, wait=False, application=None):
        """ Perform a search of one of 'my' sets """

        if (wait) and wait != 'N':
            waitflag = 'Y'
        else:
            waitflag = 'N'
        try:
            response = self.post('search_by_my_set_request', my_sets_id=set
                            , find_request_command=command, wait_flag=waitflag)
        except:
            raise
        
        group_id = response.find('find_response/group_number')
        if (group_id is not None and group_id.text):
            return MetalibGroup(session=self, id=group_id.text)
          
        elif (response.find('*/local_error') is not None):
            error_code = response.find('*/local_error/error_code').text
            error_msg = response.find('*/local_error/error_text').text
            raise MetalibServerException(error_code, error_msg)
          
        #TODO: Handle an invalid xml response (non-error)
        raise MetalibException(etree.tostring(response))
        
    
    def present(self, set_number, set_entry=None, doc_number=None, format=None,
                view=None, field=None, subfield=None, full_text=False,
                add_proxy=False, requester_ip=None):
        """ Retrieve one or more records (documents) from the result set and 
        return response in OAI-MARC-XML format"""
        params = {}
        if set_number:
            params['set_number'] = set_number
        if set_entry:
            params['set_entry'] = set_entry
        if doc_number:
            params['doc_number'] = doc_number
        if format:
            params['format'] = format
        if view:
            params['view'] = view
        if field:
            params['field'] = field
        if subfield:
            params['subfield'] = subfield
        if full_text and full_text != 'N':
            params['full_text'] = 'Y'
        if add_proxy and add_proxy != 'N':
            params['add_proxy'] = 'Y'
        if requester_ip:
            params['requester_ip'] = requester_ip
        
        command = {}
        command['present_command'] = params
        try:
            response = self.post('present_request', **command)
        except:
            raise
        
        if (response.find('*/local_error') is not None):
            error_code = response.find('*/local_error/error_code').text
            error_msg = response.find('*/local_error/error_text').text
            raise MetalibServerException(error_code, error_msg)
          
        elif (response.find('present_response') is not None):
            docs = ConvertXmlToDict(response)
            return docs
          
        #TODO: Handle an invalid xml response (non-error)
        raise MetalibException(etree.tostring(response))

        
    def retrieveClusterFacet(self,  set_number, type=None, application=None,
                             institution=None):
        """ Retrieve cluster and facet data for search results. Call after 'merge'. 
        Default value for 'type' is all. Possible values are:
         all - both cluster and facet results: Topic Cluster, Facet Year, Facet Author, Facet Journal, Facet Database, Facet Subject
         facet - all facet results: Facet Year, Facet Author, Facet Journal, Facet Database, Facet Subject
         cluster - Cluster results
         year - Facet Year results
         author - Facet Author results
         journal - Facet Journal results
         database - Facet Database results
         subject - Facet Subject results
        Institution - please specify, since the default value of 'METALIB' is quite useless
        
        """

        try:
            response = self.post('retrieve_cluster_facet_request', set_number=set_number
                             , type = type, calling_application=application
                             , institution=institution)
        except:
            raise

        if (response.find('*/local_error') is not None):
            error_code = response.find('*/local_error/error_code').text
            error_msg = response.find('*/local_error/error_text').text
            raise MetalibServerException(error_code, error_msg)
          
        elif (response.find('cluster_facet_response/cluster_facet') is not None):
            cluster_facet = ConvertXmlToDict(response)
            return cluster_facet
          
        #TODO: Handle an invalid xml response (non-error)
        raise MetalibException(etree.tostring(response))
        